package com.environment.component;

import com.environment.Environment;
import com.environment.component.enumn.ESkyPart;
import com.environment.exception.MissingComponentException;
import com.environment.unit.Component;
import com.environment.unit.IComponent;
import com.jme.animation.SpatialTransformer;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Geometry;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Dome;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.TextureState;
import com.jme.util.geom.BufferUtils;

/**
 * <code>SkyComponent</code> extends <code>Component</code> and maintains a geometry
 * structure to represent the sky of <code>Environment</code>.
 * <p>
 * The geometry structure can be a <code>Sphere</code>, a <code>Dome</code> or a
 * pre-generated <code>Geometry</code> instance.
 * <p>
 * Radius, rotation cycle, and all <code>SkyPart</code> <code>Texture</code> have
 * to be set prior to connecting to <code>Environment</code>.
 * <p>
 * <code>SkyComponent</code> does not require any connections with other
 * <code>Component</code> before initialization.
 *  
 * @author Yi Wang (Neakor)
 * @version Creation date: 04-10-08 21:50 EST
 * @version Modified date: 05-21-08 14:51 EST
 */
public class SkyComponent extends Component{
	/**
	 * Serial version.
	 */
	private static final long serialVersionUID = -4520990626382534563L;
	/**
	 * The reference to <code>Environment</code> instance.
	 */
	private Environment environment;
	/**
	 * The sky fading blend <code>ColorRGBA</code>.
	 */
	private ColorRGBA fading;
	/**
	 * The radius of the sky structure.
	 */
	private float radius;
	/**
	 * The length of one rotation cycle of the clouds in seconds.
	 */
	private float cycle;
	/**
	 * The flag indicates if the sky should use sphere structure.
	 */
	private boolean sphere;
	/**
	 * The <code>Geometry</code> of pre-generated model for sky.
	 */
	private Geometry skyModel;
	/**
	 * The upper day time base sky <code>Texture</code>.
	 */
	private Texture daySky;
	/**
	 * The upper night time base sky <code>Texture</code>.
	 */
	private Texture nightSky;
	/**
	 * The upper day time cloud layer <code>Texture</code>.
	 */
	private Texture upperDayCloud;
	/**
	 * The upper night time cloud layer <code>Texture</code>.
	 */
	private Texture upperNightCloud;
	/**
	 * The lower day time cloud layer <code>Texture</code>.
	 */
	private Texture lowerDayCloud;
	/**
	 * The lower night time cloud layer <code>Texture</code>.
	 */
	private Texture lowerNightCloud;
	
	/**
	 * Constructor of <code>SkyComponent</code>.
	 */
	public SkyComponent() {
		super(SkyComponent.class.toString());
		this.radius = 500;
		this.cycle = 600;
	}
	
	@Override
	public boolean validate() throws MissingComponentException{
		return true;
	}

	@Override
	protected void initializeDetail() {
		this.fading = new ColorRGBA(1, 1, 1, 0);
		Node sky = new Node("Sky");
		Node cloud = new Node("Cloud");
		this.attachChild(sky);
		this.attachChild(cloud);
		// Attach base sky layer.
		if(this.skyModel != null) sky.attachChild(this.buildModelSky());
		else if(!this.sphere) sky.attachChild(this.buildDomeSky());
		else sky.attachChild(this.buildSphereSky());
		// Attach cloud layer.
		cloud.attachChild(this.buildUpperCloud());
		if(this.sphere) cloud.attachChild(this.buildLowerCloud());
	}
	
	/**
	 * Build the base sky using pre-generated model.
	 * @return The <code>Geometry</code> based on the pre-generated model.
	 */
	private Spatial buildModelSky() {
		// Set up basic.
		this.skyModel.setLocalScale(new Vector3f());
		this.skyModel.setModelBound(new BoundingBox());
		this.skyModel.updateModelBound();
		this.skyModel.setIsCollidable(false);
		// Build base sky motion.
		this.setupRotation(this.cycle/2, this.cycle, this.skyModel);
		// Set up the day and night textures.
		this.setupTexture(this.daySky, this.nightSky, this.skyModel);
		return this.skyModel;
	}

	/**
	 * Build the <code>Dome</code> base sky structure.
	 * @return The <code>Dome</code> base sky structure.
	 */
	private Spatial buildDomeSky() {
		// Create the sky dome.
		Dome skyDome = new Dome("UpperBaseSky", null, 32, 32, this.radius, false);
		skyDome.setLocalTranslation(new Vector3f());
		skyDome.setModelBound(new BoundingBox());
		skyDome.updateModelBound();
		skyDome.setIsCollidable(false);
		// Build base sky motion.
		this.setupRotation(this.cycle/2, this.cycle, skyDome);
		// Set up the day and night textures.
	    this.setupTexture(this.daySky, this.nightSky, skyDome);
		return skyDome;
	}
	
	/**
	 * Build the <code>Sphere</code> base sky structure.
	 * @return The <code>Sphere</code> base sky structure.
	 */
	private Spatial buildSphereSky() {
		// Create the sky sphere.
		Sphere skySphere = new Sphere("", 32, 32, this.radius);
		skySphere.setLocalTranslation(new Vector3f());
		skySphere.setModelBound(new BoundingBox());
		skySphere.updateModelBound();
		skySphere.setIsCollidable(false);
		// Flip the normals.
		skySphere.getNormalBuffer(0).flip();
		float[] normals = new float[skySphere.getNormalBuffer(0).capacity()];
		skySphere.getNormalBuffer(0).get(normals);
		for(int i = 0; i < normals.length; i++) {
			normals[i] = normals[i] * -1.0f;
		}
		skySphere.setNormalBuffer(0, BufferUtils.createFloatBuffer(normals));
		// Build the base sky motion.
		this.setupRotation(this.cycle/2, this.cycle, skySphere);
		// Set up the day and night textures.
	    this.setupTexture(this.daySky, this.nightSky, skySphere);
	    return skySphere;
	}

	/**
	 * Build the upper cloud layer structure.
	 * @return The upper cloud layer structure.
	 */
	private Spatial buildUpperCloud() {
		// Create the upper cloud dome.
		Dome upperCloud = new Dome("UpperCloudDome", null, 32, 32, 0.8f*this.radius, false);
		upperCloud.setLocalTranslation(new Vector3f());
		upperCloud.setModelBound(new BoundingBox());
		upperCloud.updateModelBound();
		upperCloud.setIsCollidable(false);
		// Build clouds motion.
		this.setupRotation(this.cycle/3, this.cycle/1.5f, upperCloud);
		// Set up the day and night textures.
	    this.setupTexture(this.upperDayCloud, this.upperNightCloud, upperCloud);
		upperCloud.setRenderState(this.environment.getAlphaState());
		return upperCloud;
	}

	/**
	 * Build the lower cloud layer structure.
	 * @return The lower cloud layer structure.
	 */
	private Spatial buildLowerCloud() {
		// Create the lower cloud dome.
		Dome lowerCloud = new Dome("LowerCloudDome", new Vector3f(0,0,0), 32, 32, 0.8f*this.radius, false);
		lowerCloud.setModelBound(new BoundingBox());
		lowerCloud.updateModelBound();
		lowerCloud.setIsCollidable(false);
		// Rotate 180 degrees.
		Quaternion rotation = new Quaternion();
		rotation.fromAngleAxis(180*FastMath.DEG_TO_RAD, new Vector3f(1,0,0));
		lowerCloud.setLocalRotation(rotation);
		// Set up the day and night textures.
	    this.setupTexture(this.lowerDayCloud, this.lowerNightCloud, lowerCloud);
		lowerCloud.setRenderState(this.environment.getAlphaState());
		return lowerCloud;
	}
	
	@Override
	protected void updateDetail(float time) {
		// If the current time is between 4:00 and 6:00 change the sky towards day sky.
		if(time >= 4 && time <= 6) {
			this.fading.a = (time - 4)/2;
		// Day time.
		} else if(time > 6 && time < 16) {
			this.fading.a = 1;
		// Else if the current time is between 16:00 and 18:00 change the sky towards night sky.
		} else if(time >= 16 && time <= 18) {
			this.fading.a = 1 - ((time - 16)/2);
		// Night time.
		} else if(time > 18 || time < 4) {
			this.fading.a = 0;
		}
	}

	/**
	 * Setup the rotation with given half and end time on given <code>Spatial</code>.
	 * @param half The half time for one cycle of this rotation.
	 * @param end The end time for one cycle of this rotation.
	 * @param spatial The Spatial to be rotated.
	 */
	private void setupRotation(float half, float end, Spatial spatial) {
		SpatialTransformer animator = new SpatialTransformer(1);
		animator.setObject(spatial, 0, -1);
	    Quaternion start = new Quaternion();
	    start.fromAngleAxis(0, new Vector3f(0,1,0));
	    animator.setRotation(0, 0, start);
	    Quaternion middle = new Quaternion();
	    middle.fromAngleAxis(FastMath.DEG_TO_RAD*180, new Vector3f(0,-1,0));
	    animator.setRotation(0, half, middle);
	    Quaternion finish = new Quaternion();
	    finish.fromAngleAxis(FastMath.DEG_TO_RAD*360, new Vector3f(0,-1,0));
	    animator.setRotation(0, end, finish);
	    animator.setActive(true);
	    animator.setRepeatType(Controller.RT_WRAP);
	    animator.interpolateMissing();
	    spatial.addController(animator);
	}
	
	/**
	 * Setup day and night blending textures on the given <code>Geometry</code>.
	 * @param dayTexture The <code>Texture</code> used for the <code>Geometry</code> during day time.
	 * @param nightTexture The <code>Texture</code> used for the <code>Geometry</code> during night time.
	 * @param geometry The <code>Geometry</code> to be setup with the textures.
	 */
	private void setupTexture(Texture dayTexture, Texture nightTexture, Geometry geometry) {
		TextureState texState = this.environment.getRenderer().createTextureState();
		geometry.setRenderState(texState);
		texState.setTexture(nightTexture, 0);
		texState.setTexture(dayTexture, 1);
		nightTexture.setApply(Texture.AM_COMBINE);
		nightTexture.setCombineFuncRGB(Texture.ACF_INTERPOLATE);
		nightTexture.setCombineSrc0RGB(Texture.ACS_TEXTURE1);
		nightTexture.setCombineOp0RGB(Texture.ACO_SRC_COLOR);
		nightTexture.setCombineSrc1RGB(Texture.ACS_TEXTURE);
		nightTexture.setCombineOp1RGB(Texture.ACO_SRC_COLOR);
		nightTexture.setCombineSrc2RGB(Texture.ACS_CONSTANT);
		nightTexture.setCombineOp2RGB(Texture.ACO_SRC_ALPHA);
		nightTexture.setBlendColor(this.fading);
	    dayTexture.setApply(Texture.AM_COMBINE);
	    dayTexture.setCombineFuncRGB(Texture.ACF_MODULATE);
	    dayTexture.setCombineSrc0RGB(Texture.ACS_PREVIOUS);
	    dayTexture.setCombineOp0RGB(Texture.ACO_SRC_COLOR);
	    dayTexture.setCombineSrc1RGB(Texture.ACS_PRIMARY_COLOR);
	    dayTexture.setCombineOp1RGB(Texture.ACO_SRC_COLOR);
		// Apply the day texture on the geometry with blending.
	    geometry.copyTextureCoords(0, 0, 1);
	}
	
	@Override
	public void connect(IComponent component) {
		if(component instanceof Environment) {
			this.environment = (Environment)component;
		}
	}
	
	/**
	 * Set the <code>Geometry</code> of pre-generated model for sky.
	 * @param model The <code>Geometry</code> of pre-generated model for sky.
	 */
	public void setSkyModel(Geometry model) {
		this.skyModel = model;
	}

	/**
	 * Set the radius of the sky.
	 * @param radius The radius of the sky.
	 */
	public void setSkyRadius(float radius) {
		this.radius = radius;
	}
	
	/**
	 * Set the length of one rotation cycle of the clouds in seconds.
	 * @param cycle The length of one rotation cycle of the clouds in seconds.
	 */
	public void setRotationCycle(float cycle) {
		this.cycle = cycle;
	}
	
	/**
	 * Set if the sky structure should be a sphere.
	 * @param sphere True if sky structure should be a sphere. False otherwise.
	 */
	public void setUseSphere(boolean sphere) {
		this.sphere = sphere;
	}

	/**
	 * Set the given <code>ESkyPart</code> to use the given <code>Texture</code>.
	 * @param part The <code>ESkyPart</code> to be set.
	 * @param texture The <code>Texture</code> for the part.
	 */
	public void setTexture(ESkyPart part, Texture texture) {
		switch(part) {
		case DaySky:
			this.daySky = texture;
			break;
		case NightSky:
			this.nightSky = texture;
			break;
		case UpperDayCloud:
			this.upperDayCloud = texture;
			break;
		case UpperNightCloud:
			this.upperNightCloud = texture;
		case LowerDayCloud:
			this.lowerDayCloud = texture;
			break;
		case LowerNightCloud:
			this.lowerNightCloud = texture;
			break;
		}
	}
	
	/**
	 * Retrieve the radius of the sky.
	 * @return The radius of the sky.
	 */
	public float getSkyRadius() {
		return this.radius;
	}
}
